
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <style>
      body {
        font-family: Monospace;
        background-color: #000000;
        margin: 0px;
        overflow: hidden;
      }

      #info {
        color: #ffffff;

        font-family: Monospace;
        font-size: 13px;
        text-align: center;
        font-weight: bold;

        position: absolute;
        top: 0px; width: 100%;
        padding: 5px;
      }

      a {
        color: #0040ff;
      }
    </style>
  </head>
  <body>

    <script src="js/three.js"></script>
    <script src="js/SimulationShader.js"></script>
    <script src="js/GeometryUtils.js"></script>

    <script src="js/leap-0.6.4.min.js"></script>
    <script src="js/leap-plugins-0.1.6.1.js"></script>
    <script src="js/leap.rigged-hand-0.1.3.min.js"></script>

    <script src="helvetiker_bold.typeface.js"></script>

    <!-- WebGL 2 shaders -->
    <script id="vs-particles-2" type="x-shader/x-vertex">
      uniform float pointSize;

      varying vec3 vPosition;

      void main() {
        vPosition = position;
        gl_PointSize = pointSize;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    </script>

    <script id="fs-particles-2" type="x-shader/x-fragment">
      varying vec3 vPosition;

      void main() {
        float depth = smoothstep( 10.24, 1.0, gl_FragCoord.z / gl_FragCoord.w );
        gl_FragColor = vec4( (vec3(0.0, 0.03, 0.05) + (vPosition * 0.25)), depth );
      }
    </script>

    <script>
      function getQueryString(name, defaultValue) {
        var query = window.location.search.substring(1);
        var vars = query.split("&");
        for (var i = 0; i < vars.length; i++) {
          var pair = vars[i].split("=");
          if (pair[0] == name) {
            return unescape(pair[1]);
          }
        }
        return defaultValue;
      }

      function getQueryValue(name, defaultValue) {
        var value = getQueryString(name, null);
        if (value == null) {
          return defaultValue;
        }
        return parseInt(value, 10);
      }

      var container, canvas, gl;

      var scene, camera, light, renderer;
      var geometry, geometry2, originGeometry, cube, mesh, material;
      var roomMesh;

      var data, texture, points;
      var transformFeedback;
      var originBuffer;

      var controls;

      var fboParticles, rtTexturePos, rtTexturePos2, simulationShader;

      var particleCount = getQueryValue('particleCount', 500000);
      var pointSize = getQueryValue('pointSize', 1);

      var INT_MAX = 2147483647;

      var maxFingers = 42;
      var fingers = [];
      var fingertipSize = 0.18;
      var handOffset = new THREE.Vector3(0,-2.5, 0.0);
      var colliders = new Float32Array(maxFingers * 4);
      var showColliders = false;

      var leapController = new Leap.Controller();

      var isWebGL2 = false;

      function init() {
        container = document.createElement( 'div' );
        document.body.appendChild( container );

        canvas = document.createElement( 'canvas' );

        // Try creating a WebGL 2 context first
        gl = canvas.getContext( 'webgl2', { antialias: false } );
        if (!gl) {
          gl = canvas.getContext( 'experimental-webgl2', { antialias: false } );
        }

        isWebGL2 = !!gl;

        renderer = new THREE.WebGLRenderer( { canvas: canvas, context: gl } );
        renderer.setSize( window.innerWidth, window.innerHeight );
        renderer.sortObjects = false;
        container.appendChild( renderer.domElement );

        scene = new THREE.Scene();

        camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 1024 );
        camera.position.y = 1.5;
        camera.position.z = 4;
        camera.lookAt( scene.position );
        scene.add( camera );

        showColliders = getQueryValue('colliders', 0) != 0;
        var fingerMaterial = new THREE.MeshPhongMaterial({ color: 0x00DD22 });
        var fingerGeometry = new THREE.IcosahedronGeometry(1, 2);

        for (var i = 0; i < maxFingers; ++i) {
          var finger = {
            active: false,
            mesh: new THREE.Mesh(fingerGeometry, fingerMaterial)
          };
          scene.add(finger.mesh);
          fingers.push(finger);
        }

        var backImage = THREE.ImageUtils.loadTexture("media/Back.png");
        var ceilImage = THREE.ImageUtils.loadTexture("media/Ceiling.png");
        var floorImage = THREE.ImageUtils.loadTexture("media/Floor.png");

        var roomMaterialArray = [
          new THREE.MeshBasicMaterial({ map: backImage, side: THREE.BackSide }),
          new THREE.MeshBasicMaterial({ map: backImage, side: THREE.BackSide }),
          new THREE.MeshBasicMaterial({ map: ceilImage, side: THREE.BackSide }),
          new THREE.MeshBasicMaterial({ map: floorImage, side: THREE.BackSide }),
          new THREE.MeshBasicMaterial({ map: backImage, side: THREE.BackSide }),
          new THREE.MeshBasicMaterial({ map: backImage, side: THREE.BackSide })
        ];
        var roomMaterial = new THREE.MeshFaceMaterial( roomMaterialArray );
        var roomGeometry = new THREE.BoxGeometry( 10.24, 4, 5.12 );
        roomMesh = new THREE.Mesh( roomGeometry, roomMaterial );
        roomMesh.doubleSided = true;
        scene.add( roomMesh );

        if (isWebGL2) {
          // Start Creation of DataTexture
          var textGeo = new THREE.TextGeometry( "WebGL 2", {
            size: 1.0,
            height: 0.25,
            curveSegments: 0,
            font: "helvetiker",
            weight: "bold",
            style: "normal",
          });

          textGeo.computeBoundingBox();
          var bounds = textGeo.boundingBox;
          textGeo.applyMatrix( new THREE.Matrix4().makeTranslation(
            (bounds.max.x - bounds.min.x) * -0.5,
            (bounds.max.y - bounds.min.y) * -0.5,
            (bounds.max.z - bounds.min.z) * -0.5));

          points = THREE.GeometryUtils.randomPointsInGeometry( textGeo, particleCount );

          data = new Float32Array( particleCount * 4 );
          for ( var i = 0, j = 0, l = data.length; i < l; i += 4, j += 1 ) {
            data[ i ] = points[ j ].x;
            data[ i + 1 ] = points[ j ].y;
            data[ i + 2 ] = points[ j ].z;
            data[ i + 3 ] = 0.0;
          }

          var velData = new Float32Array( particleCount * 4 );
          for ( var i = 0, l = velData.length; i < l; i += 4 ) {
            velData[ i ] = (Math.random() - 0.5) * 0.004;
            velData[ i + 1 ] = (Math.random() - 0.5) * 0.004;
            velData[ i + 2 ] = (Math.random() - 0.5) * 0.004;
            velData[ i + 3 ] = 0.0;
          }

          var randomSeedData = new Uint32Array( particleCount );
          for ( var i = 0; i < randomSeedData.length; ++i ) {
            randomSeedData[ i ] = Math.random() * INT_MAX;
          }

          geometry = new THREE.BufferGeometry();
          geometry.addAttribute( 'position', new THREE.BufferAttribute( data, 4 ) );
          geometry.addAttribute( 'velocity', new THREE.BufferAttribute( velData, 4 ) );
          geometry.addAttribute( 'randomSeed', new THREE.BufferAttribute( randomSeedData, 1, false, true ) );
          geometry2 = geometry.clone();

          material = new THREE.ShaderMaterial( {
            uniforms: {
              "pointSize": { type: "f", value: pointSize }
            },
            vertexShader: document.getElementById( 'vs-particles-2' ).textContent,
            fragmentShader: document.getElementById( 'fs-particles-2' ).textContent,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            depthTest: true,
            transparent: true
          } );

          transformFeedback = gl.createTransformFeedback();

          originBuffer = gl.createBuffer();
          gl.bindBuffer(gl.ARRAY_BUFFER, originBuffer);
          gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

          simulationShader = new SimulationShader(renderer, maxFingers);
          simulationShader.setColliders(colliders);

          mesh = new THREE.PointCloud(geometry, material);
          scene.add(mesh);
        }

        leapController
          .use('handHold', {})
          .use('handEntry', {})
          .use('riggedHand', {
            parent: scene,
            camera: camera,
            scale: 0.2,
            offset: handOffset,
            materialOptions: {
              opacity: showColliders ? 0.5 : 1.0
            }
          })
          .connect();
      }

      function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize( window.innerWidth, window.innerHeight );
      }

      window.addEventListener( 'resize', onWindowResize, false );

      function fingerToCollider(i, size) {
        var finger = fingers[i];
        var o = i * 4;
        if (finger.active) {
          colliders[o] = finger.mesh.position.x;
          colliders[o+1] = finger.mesh.position.y;
          colliders[o+2] = finger.mesh.position.z;
          colliders[o+3] = size ? size : fingertipSize;
        } else {
          colliders[o+3] = -1;
        }
      }

      function fingerFromBone(bone, fi, size) {
        if (fi >= fingers.length) return;
        if (!size) { size = fingertipSize; }
        bone.updateMatrixWorld();
        fingers[fi].active = true;
        fingers[fi].mesh.visible = showColliders;
        fingers[fi].mesh.position.setFromMatrixPosition(bone.matrixWorld);
        fingers[fi].mesh.scale.set(size, size, size);
        fingerToCollider(fi, size);
      }

      function updateHands() {
        var frame = leapController.frame();

        var fi = 0;
        if (frame.hands) {
          for (var i = 0; i < frame.hands.length; ++i) {
            var hand = frame.hands[i];
            var handMesh = hand.data('riggedHand.mesh');

            fingerFromBone(handMesh, fi++, fingertipSize * 2.0);

            for (var j = 0; j < handMesh.fingers.length; ++j) {
              var meshFinger = handMesh.fingers[j];
              fingerFromBone(meshFinger.tip, fi++, fingertipSize * 0.8);
              fingerFromBone(meshFinger.dip, fi++, fingertipSize * 0.9);
              fingerFromBone(meshFinger.pip, fi++);
              fingerFromBone(meshFinger, fi++, fingertipSize * 1.1);
            }
          }
        }

        for (; fi < fingers.length; ++fi) {
          fingers[fi].active = false;
          fingers[fi].mesh.visible = false;
          fingerToCollider(fi);
        }
      }

      function transformFeedbackPass( gl, shader, source, target ) {
        if (!source || !target)
          return;

        var sourcePosAttrib = source.attributes['position'];
        var sourceVelAttrib = source.attributes['velocity'];
        var sourceRandomSeedAttrib = source.attributes['randomSeed'];
        var targetPosAttrib = target.attributes['position'];
        var targetVelAttrib = target.attributes['velocity'];
        var targetRandomSeedAttrib = target.attributes['randomSeed'];

        if (targetPosAttrib.buffer && sourcePosAttrib.buffer) {
          shader.bind();

          gl.enableVertexAttribArray( shader.attributes.position );
          gl.bindBuffer(gl.ARRAY_BUFFER, sourcePosAttrib.buffer);
          gl.vertexAttribPointer( shader.attributes.position, 4, gl.FLOAT, false, 16, 0 );

          gl.enableVertexAttribArray( shader.attributes.velocity );
          gl.bindBuffer(gl.ARRAY_BUFFER, sourceVelAttrib.buffer);
          gl.vertexAttribPointer( shader.attributes.velocity, 4, gl.FLOAT, false, 16, 0 );

          gl.enableVertexAttribArray( shader.attributes.origin );
          gl.bindBuffer(gl.ARRAY_BUFFER, originBuffer);
          gl.vertexAttribPointer( shader.attributes.origin, 4, gl.FLOAT, false, 16, 0 );

          gl.enableVertexAttribArray( shader.attributes.randomSeed );
          gl.bindBuffer(gl.ARRAY_BUFFER, sourceRandomSeedAttrib.buffer);
          gl.vertexAttribIPointer( shader.attributes.randomSeed, 1, gl.UNSIGNED_INT, 0, 0 );

          gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, targetPosAttrib.buffer);
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, targetVelAttrib.buffer);
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, targetRandomSeedAttrib.buffer);

          gl.enable(gl.RASTERIZER_DISCARD);
          gl.beginTransformFeedback(gl.POINTS);

          gl.drawArrays(gl.POINTS, 0, sourcePosAttrib.length / sourcePosAttrib.itemSize);

          gl.endTransformFeedback();
          gl.disable(gl.RASTERIZER_DISCARD);

          // Unbind the transform feedback buffer so subsequent attempts
          // to bind it to ARRAY_BUFFER work.
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, null);

          gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
        }
      }

      var frameId = 0;
      var fixedStep = 16.666666;
      function render(t) {
        requestAnimationFrame(render);

        updateHands();

        if (isWebGL2) {
          var lastTime = simulationShader.getTime(t);
          if (t - lastTime > fixedStep * 5) {
            simulationShader.setTime(t);
            lastTime = t - fixedStep;
          }

          while (t - lastTime >= fixedStep) {
            lastTime += fixedStep;
            simulationShader.setTime(lastTime);

            if ( frameId % 2 === 0 ) {
              transformFeedbackPass( renderer.context, simulationShader, geometry2, geometry );
              mesh.geometry = geometry2;
            } else {
              transformFeedbackPass( renderer.context, simulationShader, geometry, geometry2 );
              mesh.geometry = geometry;
            }

            frameId++;
          }

          // Ugly hack to make the particle mesh always draw last
          scene.remove( mesh ); scene.add( mesh );
        }

        renderer.render( scene, camera );
      }

      init();

      requestAnimationFrame(render);

    </script>
  </body>
</html>
